package org.apache.jmeter;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.Thread.UncaughtExceptionHandler;
import java.security.Permission;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import org.apache.commons.io.IOUtils;
import org.apache.jmeter.JMeter;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.tools.ant.DirectoryScanner;

/**
 * JMeter Maven plugin.
 * 
 * @author Tim McCune
 * @goal jmeter
 * @requiresDependencyResolution test
 */
public class JMeterMojo extends AbstractMojo {

	private static final Pattern PAT_ERROR = Pattern
			.compile(".*\\s+ERROR\\s+.*");

	/**
	 * @parameter
	 */
	private List<String> includes;

	/**
	 * @parameter
	 */
	private List<String> excludes;

	/**
	 * @parameter expression="${basedir}/src/test/jmeter"
	 */
	private File srcDir;

	/**
	 * @parameter expression="jmeter-reports"
	 */
	private File reportDir;

	/**
	 * @parameter expression="${basedir}/src/test/jmeter/jmeter.properties"
	 */
	private File jmeterProps;

	/**
	 * @parameter
	 */
	private boolean remote;

	/**
	 * The dir where the compiled classes are placed
	 * 
	 * @parameter expression="${jmeter.compiledClasses}"
	 *            default-value="${basedir}/target/classes"
	 */
	private File compiledClasses;

	/**
	 * The dir where the compiled test classes are placed
	 * 
	 * @parameter expression="${jmeter.compiledTestClasses}"
	 *            default-value="${basedir}/target/test-classes"
	 */
	private File compiledTestClasses;

	/**
	 * The file where the classpath is written
	 * 
	 * @parameter expression="${jmeter.classpathDump}"
	 *            default-value="${basedir}/target/classpath"
	 */
	private File classpathDump;

	private Logger logger = Logger.getLogger(JMeterMojo.class);

	private File workDir;
	private File saveServiceProps;
	private File jmeterLog;
	private DateFormat fmt = new SimpleDateFormat("yyyyMMddHHmmss");

	/**
	 * Run all JMeter tests.
	 */
	public void execute() throws MojoExecutionException, MojoFailureException {
		addDependenciesToClasspath();
		initSystemProps();

		try {
			DirectoryScanner scanner = new DirectoryScanner();
			scanner.setBasedir(srcDir);
			scanner.setIncludes(includes == null ? new String[] { "**/*.jmx" }
					: includes.toArray(new String[] {}));
			if (excludes != null) {
				scanner.setExcludes(excludes.toArray(new String[] {}));
			}
			scanner.scan();
			for (String file : scanner.getIncludedFiles()) {
				jmeterLog = new File(workDir, "jmeter." + file + ".log");
				try {
					System.setProperty("log_file", jmeterLog.getCanonicalPath());
				} catch (IOException e) {
					throw new MojoExecutionException(
							"Can't get canonical path for log file", e);
				}
				executeTest(new File(srcDir, file));
				saveTps(file.substring(0, file.lastIndexOf(".")));
			}

			checkForErrors();
		} finally {
			saveServiceProps.delete();
		}
	}

	private void checkForErrors() throws MojoExecutionException,
			MojoFailureException {
		try {
			BufferedReader in = new BufferedReader(new FileReader(jmeterLog));
			String line;
			while ((line = in.readLine()) != null) {
				if (PAT_ERROR.matcher(line).find()) {
					throw new MojoFailureException("There were test errors");
				}
			}
			in.close();
		} catch (IOException e) {
			throw new MojoExecutionException("Can't read log file", e);
		}
	}

	private void saveTps(String testName) throws MojoExecutionException {
		String tps = "-1";
		try {
			BufferedReader in = new BufferedReader(new FileReader(jmeterLog));
			String line;
			while ((line = in.readLine()) != null) {
				if (line.indexOf("jmeter.reporters.Summariser") != -1) {
					int from = line.lastIndexOf("=")+4;
					int end = line.indexOf("/s");
					tps = line.substring(from, end);
				}
			}
			in.close();
		} catch (IOException e) {
			throw new MojoExecutionException("Can't read log file", e);
		}
		try {
			if (!"-1".equals(tps)) {
				BufferedWriter out = new BufferedWriter(new FileWriter(
						reportDir.toString() + File.separator + testName
								+ ".xml.txt"));
				out.write(tps);
				out.flush();
				out.close();
				return;
			}
			throw new MojoExecutionException("Can't save the transaction per secend");
		} catch (IOException e) {
			throw new MojoExecutionException("Can't read log file", e);
		}
	}

	private void initSystemProps() throws MojoExecutionException {
		workDir = new File("target" + File.separator + "jmeter");
		workDir.mkdirs();
		createSaveServiceProps();
		/*
		 * jmeterLog = new File(workDir, "jmeter.log"); try {
		 * System.setProperty("log_file", jmeterLog.getCanonicalPath()); } catch
		 * (IOException e) { throw new
		 * MojoExecutionException("Can't get canonical path for log file", e); }
		 */
	}

	/**
	 * This mess is necessary because JMeter must load this info from a file.
	 * Resources won't work.
	 */
	private void createSaveServiceProps() throws MojoExecutionException {
		saveServiceProps = new File(workDir, "saveservice.properties");
		try {
			FileWriter out = new FileWriter(saveServiceProps);
			IOUtils.copy(Thread.currentThread().getContextClassLoader()
					.getResourceAsStream("saveservice.properties"), out);
			out.flush();
			out.close();
			System.setProperty("saveservice_properties", File.separator
					+ "target" + File.separator + "jmeter" + File.separator
					+ "saveservice.properties");
		} catch (IOException e) {
			throw new MojoExecutionException(
					"Could not create temporary saveservice.properties", e);
		}
	}

	/**
	 * Executes a single JMeter test by building up a list of command line
	 * parameters to pass to JMeter.start().
	 */
	private void executeTest(File test) throws MojoExecutionException {
		try {
			getLog().info("Executing test: " + test.getCanonicalPath());
			String reportFileName = test.getName().substring(0,
					test.getName().lastIndexOf("."))
					+ "-" + fmt.format(new Date()) + ".xml";

			// String folderName = test.getName().replace(".jmx", "");
			// folderName = folderName.substring(folderName.lastIndexOf("-") +
			// 1);

			// String finalReportDir = reportDir.toString() + File.separator +
			// folderName
			// + File.separator + reportFileName;
			String finalReportDir = reportDir.toString() + File.separator
					+ reportFileName;

			List<String> args = Arrays.asList("-n", "-t",
					test.getCanonicalPath(), "-l", finalReportDir, "-p",
					jmeterProps.toString(), "-d",
					System.getProperty("user.dir"));
			if (remote) {
				args.add("-r");
			}

			// This mess is necessary because JMeter likes to use System.exit.
			// We need to trap the exit call.
			SecurityManager oldManager = System.getSecurityManager();
			System.setSecurityManager(new SecurityManager() {
				@Override
				public void checkExit(int status) {
					throw new ExitException(status);
				}

				@Override
				public void checkPermission(Permission perm, Object context) {
				}

				@Override
				public void checkPermission(Permission perm) {
				}
			});
			UncaughtExceptionHandler oldHandler = Thread
					.getDefaultUncaughtExceptionHandler();
			Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
				public void uncaughtException(Thread t, Throwable e) {
					if (e instanceof ExitException
							&& ((ExitException) e).getCode() == 0) {
						return; // Ignore
					}
					getLog().error("Error in thread " + t.getName());
				}
			});
			try {
				// This mess is necessary because the only way to know when
				// JMeter
				// is done is to wait for all of the threads that it spawned to
				// exit.
				// int startThreadCount = Thread.activeCount();
				new JMeter().start(args.toArray(new String[] {}));

				/*
				 * while (Thread.activeCount() > startThreadCount) { try {
				 * Thread.sleep(1000); } catch (InterruptedException e) {
				 * getLog().info("InturreptedException caught"); break; } }
				 */
				BufferedReader in = new BufferedReader(
						new FileReader(jmeterLog));
				while (!checkForEndOfTest(in)) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						break;
					}
				}

				in.close();
			} catch (ExitException e) {
				if (e.getCode() != 0) {
					throw new MojoExecutionException("Test failed", e);
				}
			} finally {
				System.setSecurityManager(oldManager);
				Thread.setDefaultUncaughtExceptionHandler(oldHandler);
			}
		} catch (IOException e) {
			throw new MojoExecutionException("Can't execute test", e);
		}
	}

	private boolean checkForEndOfTest(BufferedReader in)
			throws MojoExecutionException {
		boolean testEnded = false;
		try {
			String line;
			while ((line = in.readLine()) != null) {
				if (line.indexOf("Test has ended") != -1) {
					testEnded = true;
					break;
				}
			}
		} catch (IOException e) {
			throw new MojoExecutionException("Can't read log file", e);
		}
		return testEnded;
	}

	private static class ExitException extends SecurityException {
		private static final long serialVersionUID = 5544099211927987521L;

		public int _rc;

		public ExitException(int rc) {
			super(Integer.toString(rc));
			_rc = rc;
		}

		public int getCode() {
			return _rc;
		}
	}

	private void addDependenciesToClasspath() {
		try {
			if (compiledClasses != null) {
				logger.debug("Adding depedency: " + compiledClasses);
				ClassPathHacker.addFile(compiledClasses);
			} else {
				logger.warn("No compiled classes directory specified");
			}

			if (compiledTestClasses != null) {
				logger.debug("Adding depedency: " + compiledTestClasses);
				ClassPathHacker.addFile(compiledTestClasses);
			} else {
				logger.warn("No compiled test classes directory specified");
			}

			String[] dependencies = getClasspathFiles();

			if (dependencies != null) {
				for (String dependency : dependencies) {
					if (!dependency.contains("maven-")) {
						logger.debug("Adding depedency: " + dependency);
						ClassPathHacker.addFile(dependency);
					} else {
						logger.warn("Avoiding depedency: " + dependency);
					}
				}
				// ClassPathHacker.addFile(":D:\\code\\test.jmeter.plugin\\target\\test.jmeter.plugin-1.0-SNAPSHOT.jar");
			}
		} catch (IOException e) {
			logger.warn("An error occured reading the classpath dump file", e);
		}
	}

	private String[] getClasspathFiles() throws IOException {
		if (classpathDump == null) {
			logger.warn("The classpath dump file has not been setted");

			return null;
		}

		StringBuffer fileData = new StringBuffer(1000);
		BufferedReader reader = new BufferedReader(new InputStreamReader(
				new FileInputStream(classpathDump)));

		char[] buf = new char[1024];
		int numRead = 0;
		String readData;
		while ((numRead = reader.read(buf)) != -1) {
			readData = String.valueOf(buf, 0, numRead);
			fileData.append(readData);
			buf = new char[1024];
		}
		reader.close();
		// String rtn[] =
		// fileData.toString().split(System.getProperty("path.separator"));
		String rtn[] = fileData.toString().split(
				System.getProperty("path.separator"));
		logger.debug("Dependencies found : " + rtn.length);

		return rtn;
	}

}
